{
  "cells": [
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "R2AxpObRncMd"
      },
      "source": [
        "***Copyright 2020 The TensorFlow Authors.***"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "cellView": "form",
        "id": "gQ5Kfh1YnkFS"
      },
      "outputs": [],
      "source": [
        "#@title Licensed under the Apache License, Version 2.0 (the \"License\");\n",
        "# you may not use this file except in compliance with the License.\n",
        "# You may obtain a copy of the License at\n",
        "#\n",
        "# https://www.apache.org/licenses/LICENSE-2.0\n",
        "#\n",
        "# Unless required by applicable law or agreed to in writing, software\n",
        "# distributed under the License is distributed on an \"AS IS\" BASIS,\n",
        "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n",
        "# See the License for the specific language governing permissions and\n",
        "# limitations under the License."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "uc0VwsT5nvQi"
      },
      "source": [
        "# Shape Constraints for Ethics with Tensorflow Lattice"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "gqJQZdvfn32j"
      },
      "source": [
        "<table class=\"tfo-notebook-buttons\" align=\"left\">\n",
        "  <td>\n",
        "    <a target=\"_blank\" href=\"https://www.tensorflow.org/lattice/tutorials/shape_constraints_for_ethics\"><img src=\"https://www.tensorflow.org/images/tf_logo_32px.png\" />View on TensorFlow.org</a>\n",
        "  </td>\n",
        "  <td>\n",
        "    <a target=\"_blank\" href=\"https://colab.research.google.com/github/tensorflow/lattice/blob/master/docs/tutorials/shape_constraints_for_ethics.ipynb\"><img src=\"https://www.tensorflow.org/images/colab_logo_32px.png\" />Run in Google Colab</a>\n",
        "  </td>\n",
        "  <td>\n",
        "    <a target=\"_blank\" href=\"https://github.com/tensorflow/lattice/blob/master/docs/tutorials/shape_constraints_for_ethics.ipynb\"><img src=\"https://www.tensorflow.org/images/GitHub-Mark-32px.png\" />View source on GitHub</a>\n",
        "  </td>\n",
        "  <td>\n",
        "    <a href=\"https://storage.googleapis.com/tensorflow_docs/lattice/docs/tutorials/shape_constraints_for_ethics.ipynb\"><img src=\"https://www.tensorflow.org/images/download_logo_32px.png\" />Download notebook</a>\n",
        "  </td>\n",
        "</table>"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "YFZbuZMAoBny"
      },
      "source": [
        "## Overview\n",
        "\n",
        "This tutorial demonstrates how the TensorFlow Lattice (TFL) library can be used\n",
        "to train models that behave *responsibly*, and do not violate certain\n",
        "assumptions that are *ethical* or *fair*. In particular, we will focus on using monotonicity constraints to avoid *unfair penalization* of certain attributes. This tutorial includes demonstrations\n",
        "of the experiments from the paper\n",
        "[*Deontological Ethics By Monotonicity Shape Constraints*](https://arxiv.org/abs/2001.11990)\n",
        "by Serena Wang and Maya Gupta, published at\n",
        "[AISTATS 2020](https://www.aistats.org/).\n",
        "\n",
        "We will use TFL canned estimators on public datasets, but note that\n",
        "everything in this tutorial can also be done with models constructed from TFL\n",
        "Keras layers.\n",
        "\n",
        "Before proceeding, make sure your runtime has all required packages installed\n",
        "(as imported in the code cells below)."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "o4L76T-NpgCS"
      },
      "source": [
        "## Setup"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "6FvmHcqbpkL7"
      },
      "source": [
        "Installing TF Lattice package:"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "f91yvUt_peYs"
      },
      "outputs": [],
      "source": [
        "#@test {\"skip\": true}\n",
        "!pip install tensorflow-lattice seaborn"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "6TDoQsvSpmfx"
      },
      "source": [
        "Importing required packages:"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "KGt0pm0b1O5X"
      },
      "outputs": [],
      "source": [
        "import tensorflow as tf\n",
        "\n",
        "import logging\n",
        "import matplotlib.pyplot as plt\n",
        "import numpy as np\n",
        "import os\n",
        "import pandas as pd\n",
        "import seaborn as sns\n",
        "from sklearn.model_selection import train_test_split\n",
        "import sys\n",
        "import tensorflow_lattice as tfl\n",
        "logging.disable(sys.maxsize)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "DFN6GOcBAqzv"
      },
      "source": [
        "Default values used in this tutorial:"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "9uqMM2joAnoW"
      },
      "outputs": [],
      "source": [
        "# List of learning rate hyperparameters to try.\n",
        "# For a longer list of reasonable hyperparameters, try [0.001, 0.01, 0.1].\n",
        "LEARNING_RATES = [0.01]\n",
        "# Default number of training epochs and batch sizes.\n",
        "NUM_EPOCHS = 1000\n",
        "BATCH_SIZE = 1000\n",
        "# Directory containing dataset files.\n",
        "DATA_DIR = 'https://raw.githubusercontent.com/serenalwang/shape_constraints_for_ethics/master'"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "OZJQfJvY3ibC"
      },
      "source": [
        "# Case study #1: Law school admissions\n",
        "\n",
        "In the first part of this tutorial, we will consider a case study using the Law\n",
        "School Admissions dataset from the Law School Admissions Council (LSAC). We will\n",
        "train a classifier to predict whether or not a student will pass the bar using\n",
        "two features: the student's LSAT score and undergraduate GPA.\n",
        "\n",
        "Suppose that the classifier’s score was used to guide law school admissions or\n",
        "scholarships. According to merit-based social norms, we would expect that\n",
        "students with higher GPA and higher LSAT score should receive a higher score\n",
        "from the classifier. However, we will observe that it is easy for models to\n",
        "violate these intuitive norms, and sometimes penalize people for having a higher\n",
        "GPA or LSAT score.\n",
        "\n",
        "To address this *unfair penalization* problem, we can impose monotonicity\n",
        "constraints so that a model never penalizes higher GPA or higher LSAT score, all\n",
        "else equal. In this tutorial, we will show how to impose those monotonicity\n",
        "constraints using TFL."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "vJES8lYT1fHN"
      },
      "source": [
        "## Load Law School Data"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "Cl89ZOsQ14An"
      },
      "outputs": [],
      "source": [
        "# Load data file.\n",
        "law_file_name = 'lsac.csv'\n",
        "law_file_path = os.path.join(DATA_DIR, law_file_name)\n",
        "raw_law_df = pd.read_csv(law_file_path, delimiter=',')"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "RCpTYCNjqOsC"
      },
      "source": [
        "Preprocess dataset:"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "jdY5rtLs4xQK"
      },
      "outputs": [],
      "source": [
        "# Define label column name.\n",
        "LAW_LABEL = 'pass_bar'"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "1t1Hd8gu6Uat"
      },
      "outputs": [],
      "source": [
        "def preprocess_law_data(input_df):\n",
        "  # Drop rows with where the label or features of interest are missing.\n",
        "  output_df = input_df[~input_df[LAW_LABEL].isna() & ~input_df['ugpa'].isna() &\n",
        "                       (input_df['ugpa'] > 0) & ~input_df['lsat'].isna()]\n",
        "  return output_df\n",
        "\n",
        "\n",
        "law_df = preprocess_law_data(raw_law_df)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "YhvSKr9SCrHP"
      },
      "source": [
        "### Split data into train/validation/test sets"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "gQKkIGD-CvGD"
      },
      "outputs": [],
      "source": [
        "def split_dataset(input_df, random_state=888):\n",
        "  \"\"\"Splits an input dataset into train, val, and test sets.\"\"\"\n",
        "  train_df, test_val_df = train_test_split(\n",
        "      input_df, test_size=0.3, random_state=random_state)\n",
        "  val_df, test_df = train_test_split(\n",
        "      test_val_df, test_size=0.66, random_state=random_state)\n",
        "  return train_df, val_df, test_df\n",
        "\n",
        "\n",
        "law_train_df, law_val_df, law_test_df = split_dataset(law_df)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "zObwzY7f3aLy"
      },
      "source": [
        "### Visualize data distribution\n",
        "\n",
        "First we will visualize the distribution of the data. We will plot the GPA and\n",
        "LSAT scores for all students that passed the bar and also for all students that\n",
        "did not pass the bar."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "dRAZB5cLORUG"
      },
      "outputs": [],
      "source": [
        "def plot_dataset_contour(input_df, title):\n",
        "  plt.rcParams['font.family'] = ['serif']\n",
        "  g = sns.jointplot(\n",
        "      x='ugpa',\n",
        "      y='lsat',\n",
        "      data=input_df,\n",
        "      kind='kde',\n",
        "      xlim=[1.4, 4],\n",
        "      ylim=[0, 50])\n",
        "  g.plot_joint(plt.scatter, c='b', s=10, linewidth=1, marker='+')\n",
        "  g.ax_joint.collections[0].set_alpha(0)\n",
        "  g.set_axis_labels('Undergraduate GPA', 'LSAT score', fontsize=14)\n",
        "  g.fig.suptitle(title, fontsize=14)\n",
        "  # Adust plot so that the title fits.\n",
        "  plt.subplots_adjust(top=0.9)\n",
        "  plt.show()"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "feovlsWPQhVG"
      },
      "outputs": [],
      "source": [
        "law_df_pos = law_df[law_df[LAW_LABEL] == 1]\n",
        "plot_dataset_contour(\n",
        "    law_df_pos, title='Distribution of students that passed the bar')"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "ct-2tEedU0aO"
      },
      "outputs": [],
      "source": [
        "law_df_neg = law_df[law_df[LAW_LABEL] == 0]\n",
        "plot_dataset_contour(\n",
        "    law_df_neg, title='Distribution of students that failed the bar')"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "6grrFEMPfPjk"
      },
      "source": [
        "## Train calibrated linear model to predict bar exam passage\n",
        "\n",
        "Next, we will train a *calibrated linear model* from TFL to predict whether or\n",
        "not a student will pass the bar. The two input features will be LSAT score and\n",
        "undergraduate GPA, and the training label will be whether the student passed the\n",
        "bar.\n",
        "\n",
        "We will first train a calibrated linear model without any constraints. Then, we\n",
        "will train a calibrated linear model with monotonicity constraints and observe\n",
        "the difference in the model output and accuracy."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "vrUZvP8V736o"
      },
      "source": [
        "### Helper functions for training a TFL calibrated linear estimator\n",
        "\n",
        "These functions will be used for this law school case study, as well as the\n",
        "credit default case study below."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "ollW4xAZ72kz"
      },
      "outputs": [],
      "source": [
        "def train_tfl_estimator(train_df, monotonicity, learning_rate, num_epochs,\n",
        "                        batch_size, get_input_fn,\n",
        "                        get_feature_columns_and_configs):\n",
        "  \"\"\"Trains a TFL calibrated linear estimator.\n",
        "\n",
        "  Args:\n",
        "    train_df: pandas dataframe containing training data.\n",
        "    monotonicity: if 0, then no monotonicity constraints. If 1, then all\n",
        "      features are constrained to be monotonically increasing.\n",
        "    learning_rate: learning rate of Adam optimizer for gradient descent.\n",
        "    num_epochs: number of training epochs.\n",
        "    batch_size: batch size for each epoch. None means the batch size is the full\n",
        "      dataset size.\n",
        "    get_input_fn: function that returns the input_fn for a TF estimator.\n",
        "    get_feature_columns_and_configs: function that returns TFL feature columns\n",
        "      and configs.\n",
        "\n",
        "  Returns:\n",
        "    estimator: a trained TFL calibrated linear estimator.\n",
        "\n",
        "  \"\"\"\n",
        "  feature_columns, feature_configs = get_feature_columns_and_configs(\n",
        "      monotonicity)\n",
        "\n",
        "  model_config = tfl.configs.CalibratedLinearConfig(\n",
        "      feature_configs=feature_configs, use_bias=False)\n",
        "\n",
        "  estimator = tfl.estimators.CannedClassifier(\n",
        "      feature_columns=feature_columns,\n",
        "      model_config=model_config,\n",
        "      feature_analysis_input_fn=get_input_fn(input_df=train_df, num_epochs=1),\n",
        "      optimizer=tf.keras.optimizers.Adam(learning_rate))\n",
        "\n",
        "  estimator.train(\n",
        "      input_fn=get_input_fn(\n",
        "          input_df=train_df, num_epochs=num_epochs, batch_size=batch_size))\n",
        "  return estimator\n",
        "\n",
        "\n",
        "def optimize_learning_rates(\n",
        "    train_df,\n",
        "    val_df,\n",
        "    test_df,\n",
        "    monotonicity,\n",
        "    learning_rates,\n",
        "    num_epochs,\n",
        "    batch_size,\n",
        "    get_input_fn,\n",
        "    get_feature_columns_and_configs,\n",
        "):\n",
        "  \"\"\"Optimizes learning rates for TFL estimators.\n",
        "\n",
        "  Args:\n",
        "    train_df: pandas dataframe containing training data.\n",
        "    val_df: pandas dataframe containing validation data.\n",
        "    test_df: pandas dataframe containing test data.\n",
        "    monotonicity: if 0, then no monotonicity constraints. If 1, then all\n",
        "      features are constrained to be monotonically increasing.\n",
        "    learning_rates: list of learning rates to try.\n",
        "    num_epochs: number of training epochs.\n",
        "    batch_size: batch size for each epoch. None means the batch size is the full\n",
        "      dataset size.\n",
        "    get_input_fn: function that returns the input_fn for a TF estimator.\n",
        "    get_feature_columns_and_configs: function that returns TFL feature columns\n",
        "      and configs.\n",
        "\n",
        "  Returns:\n",
        "    A single TFL estimator that achieved the best validation accuracy.\n",
        "  \"\"\"\n",
        "  estimators = []\n",
        "  train_accuracies = []\n",
        "  val_accuracies = []\n",
        "  test_accuracies = []\n",
        "  for lr in learning_rates:\n",
        "    estimator = train_tfl_estimator(\n",
        "        train_df=train_df,\n",
        "        monotonicity=monotonicity,\n",
        "        learning_rate=lr,\n",
        "        num_epochs=num_epochs,\n",
        "        batch_size=batch_size,\n",
        "        get_input_fn=get_input_fn,\n",
        "        get_feature_columns_and_configs=get_feature_columns_and_configs)\n",
        "    estimators.append(estimator)\n",
        "    train_acc = estimator.evaluate(\n",
        "        input_fn=get_input_fn(train_df, num_epochs=1))['accuracy']\n",
        "    val_acc = estimator.evaluate(\n",
        "        input_fn=get_input_fn(val_df, num_epochs=1))['accuracy']\n",
        "    test_acc = estimator.evaluate(\n",
        "        input_fn=get_input_fn(test_df, num_epochs=1))['accuracy']\n",
        "    print('accuracies for learning rate %f: train: %f, val: %f, test: %f' %\n",
        "          (lr, train_acc, val_acc, test_acc))\n",
        "    train_accuracies.append(train_acc)\n",
        "    val_accuracies.append(val_acc)\n",
        "    test_accuracies.append(test_acc)\n",
        "  max_index = val_accuracies.index(max(val_accuracies))\n",
        "  return estimators[max_index]"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "jeEfKSA7_aOg"
      },
      "source": [
        "### Helper functions for configuring law school dataset features\n",
        "\n",
        "These helper functions are specific to the law school case study."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "B6NU6EEKIYMJ"
      },
      "outputs": [],
      "source": [
        "def get_input_fn_law(input_df, num_epochs, batch_size=None):\n",
        "  \"\"\"Gets TF input_fn for law school models.\"\"\"\n",
        "  return tf.compat.v1.estimator.inputs.pandas_input_fn(\n",
        "      x=input_df[['ugpa', 'lsat']],\n",
        "      y=input_df['pass_bar'],\n",
        "      num_epochs=num_epochs,\n",
        "      batch_size=batch_size or len(input_df),\n",
        "      shuffle=False)\n",
        "\n",
        "\n",
        "def get_feature_columns_and_configs_law(monotonicity):\n",
        "  \"\"\"Gets TFL feature configs for law school models.\"\"\"\n",
        "  feature_columns = [\n",
        "      tf.feature_column.numeric_column('ugpa'),\n",
        "      tf.feature_column.numeric_column('lsat'),\n",
        "  ]\n",
        "  feature_configs = [\n",
        "      tfl.configs.FeatureConfig(\n",
        "          name='ugpa',\n",
        "          lattice_size=2,\n",
        "          pwl_calibration_num_keypoints=20,\n",
        "          monotonicity=monotonicity,\n",
        "          pwl_calibration_always_monotonic=False),\n",
        "      tfl.configs.FeatureConfig(\n",
        "          name='lsat',\n",
        "          lattice_size=2,\n",
        "          pwl_calibration_num_keypoints=20,\n",
        "          monotonicity=monotonicity,\n",
        "          pwl_calibration_always_monotonic=False),\n",
        "  ]\n",
        "  return feature_columns, feature_configs"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "HSfAwgiO_6YA"
      },
      "source": [
        "### Helper functions for visualization of trained model outputs"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "HESNIC5H-1dz"
      },
      "outputs": [],
      "source": [
        "def get_predicted_probabilities(estimator, input_df, get_input_fn):\n",
        "  predictions = estimator.predict(\n",
        "      input_fn=get_input_fn(input_df=input_df, num_epochs=1))\n",
        "  return [prediction['probabilities'][1] for prediction in predictions]\n",
        "\n",
        "\n",
        "def plot_model_contour(estimator, input_df, num_keypoints=20):\n",
        "  x = np.linspace(min(input_df['ugpa']), max(input_df['ugpa']), num_keypoints)\n",
        "  y = np.linspace(min(input_df['lsat']), max(input_df['lsat']), num_keypoints)\n",
        "\n",
        "  x_grid, y_grid = np.meshgrid(x, y)\n",
        "\n",
        "  positions = np.vstack([x_grid.ravel(), y_grid.ravel()])\n",
        "  plot_df = pd.DataFrame(positions.T, columns=['ugpa', 'lsat'])\n",
        "  plot_df[LAW_LABEL] = np.ones(len(plot_df))\n",
        "  predictions = get_predicted_probabilities(\n",
        "      estimator=estimator, input_df=plot_df, get_input_fn=get_input_fn_law)\n",
        "  grid_predictions = np.reshape(predictions, x_grid.shape)\n",
        "\n",
        "  plt.rcParams['font.family'] = ['serif']\n",
        "  plt.contour(\n",
        "      x_grid,\n",
        "      y_grid,\n",
        "      grid_predictions,\n",
        "      colors=('k',),\n",
        "      levels=np.linspace(0, 1, 11))\n",
        "  plt.contourf(\n",
        "      x_grid,\n",
        "      y_grid,\n",
        "      grid_predictions,\n",
        "      cmap=plt.cm.bone,\n",
        "      levels=np.linspace(0, 1, 11))  # levels=np.linspace(0,1,8));\n",
        "  plt.xticks(fontsize=20)\n",
        "  plt.yticks(fontsize=20)\n",
        "\n",
        "  cbar = plt.colorbar()\n",
        "  cbar.ax.set_ylabel('Model score', fontsize=20)\n",
        "  cbar.ax.tick_params(labelsize=20)\n",
        "\n",
        "  plt.xlabel('Undergraduate GPA', fontsize=20)\n",
        "  plt.ylabel('LSAT score', fontsize=20)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "fAMSCaRHIn1w"
      },
      "source": [
        "## Train unconstrained (non-monotonic) calibrated linear model"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "Iff8omH3Ij_x"
      },
      "outputs": [],
      "source": [
        "nomon_linear_estimator = optimize_learning_rates(\n",
        "    train_df=law_train_df,\n",
        "    val_df=law_val_df,\n",
        "    test_df=law_test_df,\n",
        "    monotonicity=0,\n",
        "    learning_rates=LEARNING_RATES,\n",
        "    batch_size=BATCH_SIZE,\n",
        "    num_epochs=NUM_EPOCHS,\n",
        "    get_input_fn=get_input_fn_law,\n",
        "    get_feature_columns_and_configs=get_feature_columns_and_configs_law)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "Gxfv8hXMh4_E"
      },
      "outputs": [],
      "source": [
        "plot_model_contour(nomon_linear_estimator, input_df=law_df)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "eKVkjHg_LaWb"
      },
      "source": [
        "## Train monotonic calibrated linear model"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "C_MUEvGNp6g2"
      },
      "outputs": [],
      "source": [
        "mon_linear_estimator = optimize_learning_rates(\n",
        "    train_df=law_train_df,\n",
        "    val_df=law_val_df,\n",
        "    test_df=law_test_df,\n",
        "    monotonicity=1,\n",
        "    learning_rates=LEARNING_RATES,\n",
        "    batch_size=BATCH_SIZE,\n",
        "    num_epochs=NUM_EPOCHS,\n",
        "    get_input_fn=get_input_fn_law,\n",
        "    get_feature_columns_and_configs=get_feature_columns_and_configs_law)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "ABdhYOUVCXzD"
      },
      "outputs": [],
      "source": [
        "plot_model_contour(mon_linear_estimator, input_df=law_df)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "fsI14lrFxRha"
      },
      "source": [
        "## Train other unconstrained models\n",
        "\n",
        "We demonstrated that TFL calibrated linear models could be trained to be\n",
        "monotonic in both LSAT score and GPA without too big of a sacrifice in accuracy.\n",
        "\n",
        "But, how does the calibrated linear model compare to other types of models, like\n",
        "deep neural networks (DNNs) or gradient boosted trees (GBTs)? Do DNNs and GBTs\n",
        "appear to have reasonably fair outputs? To address this question, we will next\n",
        "train an unconstrained DNN and GBT. In fact, we will observe that the DNN and\n",
        "GBT both easily violate monotonicity in LSAT score and undergraduate GPA."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "uo1ruWXcvUqb"
      },
      "source": [
        "### Train an unconstrained Deep Neural Network (DNN) model\n",
        "\n",
        "The architecture was previously optimized to achieve high validation accuracy."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "3pplraob0Od-"
      },
      "outputs": [],
      "source": [
        "feature_names = ['ugpa', 'lsat']\n",
        "\n",
        "dnn_estimator = tf.estimator.DNNClassifier(\n",
        "    feature_columns=[\n",
        "        tf.feature_column.numeric_column(feature) for feature in feature_names\n",
        "    ],\n",
        "    hidden_units=[100, 100],\n",
        "    optimizer=tf.keras.optimizers.Adam(learning_rate=0.008),\n",
        "    activation_fn=tf.nn.relu)\n",
        "\n",
        "dnn_estimator.train(\n",
        "    input_fn=get_input_fn_law(\n",
        "        law_train_df, batch_size=BATCH_SIZE, num_epochs=NUM_EPOCHS))\n",
        "dnn_train_acc = dnn_estimator.evaluate(\n",
        "    input_fn=get_input_fn_law(law_train_df, num_epochs=1))['accuracy']\n",
        "dnn_val_acc = dnn_estimator.evaluate(\n",
        "    input_fn=get_input_fn_law(law_val_df, num_epochs=1))['accuracy']\n",
        "dnn_test_acc = dnn_estimator.evaluate(\n",
        "    input_fn=get_input_fn_law(law_test_df, num_epochs=1))['accuracy']\n",
        "print('accuracies for DNN: train: %f, val: %f, test: %f' %\n",
        "      (dnn_train_acc, dnn_val_acc, dnn_test_acc))"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "LwPQqLt-E7R4"
      },
      "outputs": [],
      "source": [
        "plot_model_contour(dnn_estimator, input_df=law_df)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "OOAKK0_3vWir"
      },
      "source": [
        "### Train an unconstrained Gradient Boosted Trees (GBT) model\n",
        "\n",
        "The tree structure was previously optimized to achieve high validation accuracy."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "mFaI9hB-rgoL"
      },
      "outputs": [],
      "source": [
        "tree_estimator = tf.estimator.BoostedTreesClassifier(\n",
        "    feature_columns=[\n",
        "        tf.feature_column.numeric_column(feature) for feature in feature_names\n",
        "    ],\n",
        "    n_batches_per_layer=2,\n",
        "    n_trees=20,\n",
        "    max_depth=4)\n",
        "\n",
        "tree_estimator.train(\n",
        "    input_fn=get_input_fn_law(\n",
        "        law_train_df, num_epochs=NUM_EPOCHS, batch_size=BATCH_SIZE))\n",
        "tree_train_acc = tree_estimator.evaluate(\n",
        "    input_fn=get_input_fn_law(law_train_df, num_epochs=1))['accuracy']\n",
        "tree_val_acc = tree_estimator.evaluate(\n",
        "    input_fn=get_input_fn_law(law_val_df, num_epochs=1))['accuracy']\n",
        "tree_test_acc = tree_estimator.evaluate(\n",
        "    input_fn=get_input_fn_law(law_test_df, num_epochs=1))['accuracy']\n",
        "print('accuracies for GBT: train: %f, val: %f, test: %f' %\n",
        "      (tree_train_acc, tree_val_acc, tree_test_acc))"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "AZFyfQT1E_nR"
      },
      "outputs": [],
      "source": [
        "plot_model_contour(tree_estimator, input_df=law_df)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "uX2qiMlrY8aO"
      },
      "source": [
        "# Case study #2: Credit Default\n",
        "\n",
        "The second case study that we will consider in this tutorial is predicting an\n",
        "individual's credit default probability. We will use the Default of Credit Card\n",
        "Clients dataset from the UCI repository. This data was collected from 30,000\n",
        "Taiwanese credit card users and contains a binary label of whether or not a user\n",
        "defaulted on a payment in a time window. Features include marital status,\n",
        "gender, education, and how long a user is behind on payment of their existing\n",
        "bills, for each of the months of April-September 2005.\n",
        "\n",
        "As we did with the first case study, we again illustrate using monotonicity\n",
        "constraints to avoid *unfair penalization*: if the model were to be used to\n",
        "determine a user’s credit score, it could feel unfair to many if they were\n",
        "penalized for paying their bills sooner, all else equal. Thus, we apply a\n",
        "monotonicity constraint that keeps the model from penalizing early payments."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "tz5yduNuFinA"
      },
      "source": [
        "## Load Credit Default data"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "KuylMNBCILwy"
      },
      "outputs": [],
      "source": [
        "# Load data file.\n",
        "credit_file_name = 'credit_default.csv'\n",
        "credit_file_path = os.path.join(DATA_DIR, credit_file_name)\n",
        "credit_df = pd.read_csv(credit_file_path, delimiter=',')"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "Hv_GQcEHIf9v"
      },
      "outputs": [],
      "source": [
        "# Define label column name.\n",
        "CREDIT_LABEL = 'default'"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "13oZWY0YIoy3"
      },
      "source": [
        "### Split data into train/validation/test sets"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "dty5tXJqIscz"
      },
      "outputs": [],
      "source": [
        "credit_train_df, credit_val_df, credit_test_df = split_dataset(credit_df)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "_kAciWXHKGV7"
      },
      "source": [
        "### Visualize data distribution\n",
        "\n",
        "First we will visualize the distribution of the data. We will plot the mean and\n",
        "standard error of the observed default rate for people with different marital\n",
        "statuses and repayment statuses. The repayment status represents the number of\n",
        "months a person is behind on paying back their loan (as of April 2005)."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "8CxacQxnkHWE"
      },
      "outputs": [],
      "source": [
        "def get_agg_data(df, x_col, y_col, bins=11):\n",
        "  xbins = pd.cut(df[x_col], bins=bins)\n",
        "  data = df[[x_col, y_col]].groupby(xbins).agg(['mean', 'sem'])\n",
        "  return data\n",
        "\n",
        "\n",
        "def plot_2d_means_credit(input_df, x_col, y_col, x_label, y_label):\n",
        "  plt.rcParams['font.family'] = ['serif']\n",
        "  _, ax = plt.subplots(nrows=1, ncols=1)\n",
        "  plt.setp(ax.spines.values(), color='black', linewidth=1)\n",
        "  ax.tick_params(\n",
        "      direction='in', length=6, width=1, top=False, right=False, labelsize=18)\n",
        "  df_single = get_agg_data(input_df[input_df['MARRIAGE'] == 1], x_col, y_col)\n",
        "  df_married = get_agg_data(input_df[input_df['MARRIAGE'] == 2], x_col, y_col)\n",
        "  ax.errorbar(\n",
        "      df_single[(x_col, 'mean')],\n",
        "      df_single[(y_col, 'mean')],\n",
        "      xerr=df_single[(x_col, 'sem')],\n",
        "      yerr=df_single[(y_col, 'sem')],\n",
        "      color='orange',\n",
        "      marker='s',\n",
        "      capsize=3,\n",
        "      capthick=1,\n",
        "      label='Single',\n",
        "      markersize=10,\n",
        "      linestyle='')\n",
        "  ax.errorbar(\n",
        "      df_married[(x_col, 'mean')],\n",
        "      df_married[(y_col, 'mean')],\n",
        "      xerr=df_married[(x_col, 'sem')],\n",
        "      yerr=df_married[(y_col, 'sem')],\n",
        "      color='b',\n",
        "      marker='^',\n",
        "      capsize=3,\n",
        "      capthick=1,\n",
        "      label='Married',\n",
        "      markersize=10,\n",
        "      linestyle='')\n",
        "  leg = ax.legend(loc='upper left', fontsize=18, frameon=True, numpoints=1)\n",
        "  ax.set_xlabel(x_label, fontsize=18)\n",
        "  ax.set_ylabel(y_label, fontsize=18)\n",
        "  ax.set_ylim(0, 1.1)\n",
        "  ax.set_xlim(-2, 8.5)\n",
        "  ax.patch.set_facecolor('white')\n",
        "  leg.get_frame().set_edgecolor('black')\n",
        "  leg.get_frame().set_facecolor('white')\n",
        "  leg.get_frame().set_linewidth(1)\n",
        "  plt.show()"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "VHXyYbyekKLT"
      },
      "outputs": [],
      "source": [
        "plot_2d_means_credit(credit_train_df, 'PAY_0', 'default',\n",
        "                     'Repayment Status (April)', 'Observed default rate')"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "4hnZBigB7kzY"
      },
      "source": [
        "## Train calibrated linear model to predict credit default rate\n",
        "\n",
        "Next, we will train a *calibrated linear model* from TFL to predict whether or\n",
        "not a person will default on a loan. The two input features will be the person's\n",
        "marital status and how many months the person is behind on paying back their\n",
        "loans in April (repayment status). The training label will be whether or not the\n",
        "person defaulted on a loan.\n",
        "\n",
        "We will first train a calibrated linear model without any constraints. Then, we\n",
        "will train a calibrated linear model with monotonicity constraints and observe\n",
        "the difference in the model output and accuracy."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "UEcHW1u3Jk_2"
      },
      "source": [
        "### Helper functions for configuring credit default dataset features\n",
        "\n",
        "These helper functions are specific to the credit default case study."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "QBa-hczLi7DM"
      },
      "outputs": [],
      "source": [
        "def get_input_fn_credit(input_df, num_epochs, batch_size=None):\n",
        "  \"\"\"Gets TF input_fn for credit default models.\"\"\"\n",
        "  return tf.compat.v1.estimator.inputs.pandas_input_fn(\n",
        "      x=input_df[['MARRIAGE', 'PAY_0']],\n",
        "      y=input_df['default'],\n",
        "      num_epochs=num_epochs,\n",
        "      batch_size=batch_size or len(input_df),\n",
        "      shuffle=False)\n",
        "\n",
        "\n",
        "def get_feature_columns_and_configs_credit(monotonicity):\n",
        "  \"\"\"Gets TFL feature configs for credit default models.\"\"\"\n",
        "  feature_columns = [\n",
        "      tf.feature_column.numeric_column('MARRIAGE'),\n",
        "      tf.feature_column.numeric_column('PAY_0'),\n",
        "  ]\n",
        "  feature_configs = [\n",
        "      tfl.configs.FeatureConfig(\n",
        "          name='MARRIAGE',\n",
        "          lattice_size=2,\n",
        "          pwl_calibration_num_keypoints=3,\n",
        "          monotonicity=monotonicity,\n",
        "          pwl_calibration_always_monotonic=False),\n",
        "      tfl.configs.FeatureConfig(\n",
        "          name='PAY_0',\n",
        "          lattice_size=2,\n",
        "          pwl_calibration_num_keypoints=10,\n",
        "          monotonicity=monotonicity,\n",
        "          pwl_calibration_always_monotonic=False),\n",
        "  ]\n",
        "  return feature_columns, feature_configs"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "iwxnlRrQPdTg"
      },
      "source": [
        "### Helper functions for visualization of trained model outputs"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "zVGxEfbhPZ5H"
      },
      "outputs": [],
      "source": [
        "def plot_predictions_credit(input_df,\n",
        "                            estimator,\n",
        "                            x_col,\n",
        "                            x_label='Repayment Status (April)',\n",
        "                            y_label='Predicted default probability'):\n",
        "  predictions = get_predicted_probabilities(\n",
        "      estimator=estimator, input_df=input_df, get_input_fn=get_input_fn_credit)\n",
        "  new_df = input_df.copy()\n",
        "  new_df.loc[:, 'predictions'] = predictions\n",
        "  plot_2d_means_credit(new_df, x_col, 'predictions', x_label, y_label)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "UMIpywE1P07H"
      },
      "source": [
        "## Train unconstrained (non-monotonic) calibrated linear model"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "FfXUKns5cPEw"
      },
      "outputs": [],
      "source": [
        "nomon_linear_estimator = optimize_learning_rates(\n",
        "    train_df=credit_train_df,\n",
        "    val_df=credit_val_df,\n",
        "    test_df=credit_test_df,\n",
        "    monotonicity=0,\n",
        "    learning_rates=LEARNING_RATES,\n",
        "    batch_size=BATCH_SIZE,\n",
        "    num_epochs=NUM_EPOCHS,\n",
        "    get_input_fn=get_input_fn_credit,\n",
        "    get_feature_columns_and_configs=get_feature_columns_and_configs_credit)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "5zQ_jm75kRX6"
      },
      "outputs": [],
      "source": [
        "plot_predictions_credit(credit_train_df, nomon_linear_estimator, 'PAY_0')"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "0aokp7qLQBIr"
      },
      "source": [
        "## Train monotonic calibrated linear model"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "wWCG7YrLUZDH"
      },
      "outputs": [],
      "source": [
        "mon_linear_estimator = optimize_learning_rates(\n",
        "    train_df=credit_train_df,\n",
        "    val_df=credit_val_df,\n",
        "    test_df=credit_test_df,\n",
        "    monotonicity=1,\n",
        "    learning_rates=LEARNING_RATES,\n",
        "    batch_size=BATCH_SIZE,\n",
        "    num_epochs=NUM_EPOCHS,\n",
        "    get_input_fn=get_input_fn_credit,\n",
        "    get_feature_columns_and_configs=get_feature_columns_and_configs_credit)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "JCQ2eMdndFhR"
      },
      "outputs": [],
      "source": [
        "plot_predictions_credit(credit_train_df, mon_linear_estimator, 'PAY_0')"
      ]
    }
  ],
  "metadata": {
    "colab": {
      "collapsed_sections": [],
      "name": "shape_constraints_for_ethics.ipynb",
      "toc_visible": true
    },
    "kernelspec": {
      "display_name": "Python 3",
      "name": "python3"
    }
  },
  "nbformat": 4,
  "nbformat_minor": 0
}
